//An extremely tiny and ill-featured camera class.
//Idea is you:

//call oglCameraLookAt(...) inside your display function, after gluPerspective.
//call oglCameraInit() inside your main function

//This file doesn't depend on anything else except glut and vec3.
#pragma once

#include "OrionFlames.h"
#include "osl/vec4.h"

#define V3_2_XYZ(v) (v).x,(v).y,(v).z //Convert a vec3 to three float arguments.

namespace OrionFlames
{
//3D orientation.
//Orthonormal coordinate frame.
class OFAPI ortho_frame
{
public:
	//Unit vectors pointing along axes of our frame.
	//X cross Y is Z. (right-handed coordinate system).
	ortho_frame() : x(1, 0, 0), y(0, 0, 1), z(0, -1, 0) { }//Point down Y axis.
	
	//Reorient this coordinate frame by this far along in the X and Y axes.  
	//"dx" is the distance along the z axis to push the x axis; 
	//"dy" the distance along the z axis to push the y axis.
	void nudge(double dx, double dy)
	{
		x += dx * z;
		y += dy * z;
		orthonormalize();
	}
	
	//Reconstruct an orthonormal frame from X and Y axes.
	//Y is primary, X is secondary, Z is tertiary.
	void orthonormalize()
	{
		y = normalize(y);
		z = normalize(cross(x, y));
		x = normalize(cross(y, z));
	}

	vec3 x,y,z;
};

static ortho_frame camera_orient; //camera orientation
static ortho_frame slow_orient; //averaged camera orientation
static vec3 camera(0, 0, 0); //position of camera (set up in main)
static bool key_down[256]; //if true, the corresponding key is down
static int key_down_count = 0; //number of keys currently pressed
static bool key_first_down = false; //redisplay just requested by keyboard function
static int mouse_down_count = 0; //number of buttons currently pressed
static bool correct_head_tilt = true; //force camera X axis to remain level
static bool correct_head_tilt_sphere = false; //stay level with planet (at origin)

//Camera moves at the given real velocity in world-units per second.
static void oglCameraLookAt(float velocity = 3.0)
{
	static float display_last_t = 0.0;
	glFinish(); //Makes timer below run *way* smoother...
	float display_cur_t = (float)0.001 * glutGet(GLUT_ELAPSED_TIME);
	float display_dt = display_cur_t - display_last_t;//Seconds per frame.
	display_last_t = display_cur_t;

	if (key_first_down)
	{
		display_dt = 0.0;//Avoid jerky startup.
		key_first_down = false;
	}

	if (key_down_count > 0 || mouse_down_count > 0)//auto animate
	{
		glutPostRedisplay();
	}
	
	//Physics and display timesteps:
	//printf("dt=%.3f ms\n",display_dt);
	float vel = velocity * display_dt;//Meters camera motion per frame.
	if (key_down[(int)'w']) camera -= vel * camera_orient.z;
	if (key_down[(int)'s']) camera += vel * camera_orient.z;
	if (key_down[(int)'a']) camera -= vel * camera_orient.x;
	if (key_down[(int)'d']) camera += vel * camera_orient.x;
	if (key_down[(int)'q']) camera += vel * camera_orient.y;
	if (key_down[(int)'z']) camera -= vel * camera_orient.y;
	
	if (key_down[(int)0x1B])
		exit(0);//Escape key.
	
	if (correct_head_tilt)
	{
		//Don't allow head tilting!
		camera_orient.x.z = 0.0; 
		camera_orient.y=cross(camera_orient.z, camera_orient.x);
		camera_orient.orthonormalize();
	}

	if (correct_head_tilt_sphere)
	{
		vec3 upvector = normalize(camera);
		camera_orient.x -= upvector * dot(upvector, camera_orient.x);
		camera_orient.y = cross(camera_orient.z, camera_orient.x);
		//camera_orient.y=camera; //Y axis always faces away from origin.
		camera_orient.orthonormalize();
	}
	
	//Slowly blend in new camera orientation (smooth mouse jerks).
	float lerpfrac = (float)(1.0 - exp(-10.0 * display_dt));//Mouse motion half-life.
	lerpfrac = clamp(lerpfrac, (float)0.01, (float)1.0);
	slow_orient.x = mix(slow_orient.x, camera_orient.x, lerpfrac);
	slow_orient.y = mix(slow_orient.y, camera_orient.y, lerpfrac);
	slow_orient.z = mix(slow_orient.z, camera_orient.z, lerpfrac);
	
	vec3 target = camera - slow_orient.z;//Camera looks down *negative* Z.
	gluLookAt(V3_2_XYZ(camera), 
		V3_2_XYZ(target), 
		V3_2_XYZ(slow_orient.y)
	);
}

static int mouse_x = 0;
static int mouse_y = 0;

//Mouse being pressed or released--save position for motion.
static void camera_mouse(int button,int state,int x,int y) 
{ 
	mouse_x = x;
	mouse_y = y;

	if (state == GLUT_DOWN)
	{
		mouse_down_count++;
	}
	else
	{
		mouse_down_count--;
		if (mouse_down_count < 0)
			mouse_down_count = 0;
	}

	glutPostRedisplay();
	
}

static void camera_motion(int x, int y)
{ 
	float scale = (float)0.005;//Radians rotation/frame per mouse pixel motion.
	camera_orient.nudge(
		-scale * (x - mouse_x), 
		scale  * (y - mouse_y));

	mouse_x =x;
	mouse_y = y;//Save old mouse positions.
}

static void camera_keyboard(unsigned char key, int x, int y)
{
	key_down[key] = true;
	key_down_count++;

	if (key_down_count == 1)
		key_first_down = true;

	glutPostRedisplay();
}

static void camera_keyboardUp(unsigned char key, int x, int y)
{
	key_down[key] = false;
	key_down_count--;
	if (key_down_count < 0)
		key_down_count = 0;
}

static void oglCameraInit()
{
	glutMouseFunc(camera_mouse);
	glutMotionFunc(camera_motion); 
	glutKeyboardFunc(camera_keyboard);
	glutKeyboardUpFunc(camera_keyboardUp);
	glutSetKeyRepeat(GLUT_KEY_REPEAT_OFF);
}

//Draw 3D axes on the ground.
static void oglCameraAxes()
{
	for (int axis = 0; axis < 3; axis++)
	{
		vec3 color(0.0);
		color[axis] = 1.0;
		glColor3fv(color);
		
		glBegin(GL_LINES);
		glVertex3f(0, 0, 0);
		vec3 end(0.0);
		end[axis] = 1.0;
		glVertex3fv(end);
		glEnd();
		
		glRasterPos3fv(end);
		glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, 'X' + axis);
	}
}
}